﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using System.Runtime.InteropServices;
using System.IO;
using E4D.Solutions.SPRNuGet.EventRecievers.Helpers;
using System.Web;
using System.IO.Packaging;
using E4D.Solutions.SPRNuGet.EventReceivers.Helpers;
using System.Xml.Linq;
using System.Xml;
using System.Xml.Schema;
using E4D.Solutions.SPRNuGet.EventReceivers.Entities;
using Microsoft.SharePoint.Utilities;

namespace E4D.Solutions.SPRNuGet.EventReceivers
{
    [Guid("2C631BAF-F953-4EC3-BA9C-3B7B45B31EFC")]
    class SPRNuGetItemEventReceiver : SPItemEventReceiver
    {
        #region privates and internal variables
        private HttpContext _context;
        private const string SchemaVersionAttributeName = "schemaVersion";
        private DateTime? _packPublished;
        #endregion

        public SPRNuGetItemEventReceiver()
        {
            _context = HttpContext.Current;
        }

        public override void ItemAdding(SPItemEventProperties properties)
        {
            #region check for valid nuget package and extract its info
            string uploadedPackageName = Path.GetFileName(properties.BeforeUrl);

            HttpFileCollection collection = _context.Request.Files;
            for (int i = 0; i < collection.Count; i++)
            {
                if (uploadedPackageName.Equals(Path.GetFileName(collection[i].FileName), StringComparison.OrdinalIgnoreCase))
                {
                    Stream fileStream = collection[i].InputStream;
                    _packPublished = GetLastModified(collection[i].FileName);

                    fileStream.Position = 0;
                    byte[] fileContents = new byte[collection[i].ContentLength];
                    fileStream.Read(fileContents, 0, collection[i].ContentLength);
                    CryptoHashProvider hashHelper = new CryptoHashProvider();
                    string hash = Convert.ToBase64String(hashHelper.CalculateHash(fileContents));
                    properties.AfterProperties["NugHash"] = hash;
                    properties.AfterProperties["NugHashAlgo"] = "SHA512";
                    properties.AfterProperties["NugPackSize"] = fileStream.Length.ToString();

                    /* properties extraction */
                    Package package = Package.Open(fileStream);
                    PackageRelationship relationshipType = package.GetRelationshipsByType(Constants.PackageRelationshipNamespace + "manifest").SingleOrDefault();
                    PackagePart manifestPart = package.GetPart(relationshipType.TargetUri);
                    using (Stream manifestStream = manifestPart.GetStream())
                    {
                        XDocument document;
                        using (XmlReader reader = XmlReader.Create(manifestStream))
                        {
                            document= XDocument.Load(reader);
                            string schemaNamespace = GetSchemaNamespace(document);
                            foreach (var e in document.Descendants())
                            {
                                // Assign the schema namespace derived to all nodes in the document.
                                e.Name = XName.Get(e.Name.LocalName, schemaNamespace);
                            }

                            // Validate the schema
                            bool isValidated = ValidateManifestSchema(document, schemaNamespace, properties.Web.Site.Url);
                            if (!isValidated)
                            {
                                properties.Cancel = true;
                                properties.ErrorMessage = "Invalid NuGet package!";
                                properties.Status = SPEventReceiverStatus.CancelWithError;
                            }

                            // Serialize it
                            XElement metaData = document.Root.ElementsNoNamespace("metadata").First();
                            XNode node = metaData.FirstNode;
                            while (node != null)
                            {
                                var element = node as XElement;
                                if (element != null)
                                {
                                    SetMetadataColumn(element,ref properties);
                                }
                                node = node.NextNode;
                            }
                            if(_packPublished.HasValue)
                                properties.AfterProperties["NugPublished"] = SPUtility.CreateISO8601DateTimeFromSystemDateTime((DateTime)_packPublished);
                            properties.AfterProperties["NugLastVersion"] = "True";
                            properties.AfterProperties["NugAbsLastVersion"] = "True";
                            properties.AfterProperties["NugDownload"] = 0;
                        } 

                    }
                }
            }
            #endregion
            base.ItemAdding(properties);
        }

        private void SetMetadataColumn(XElement element, ref SPItemEventProperties properties)
        {
            switch (element.Name.LocalName)
            {
                case "version":
                    properties.AfterProperties["NugVersion"] = element.Value;
                    break;
                case "licenseUrl":
                    properties.AfterProperties["NugLicenseUrl"] = element.Value;
                    break;
                case "projectUrl":
                    properties.AfterProperties["NugProjectUrl"]=element.Value;
                    break;
                case "iconUrl":
                    properties.AfterProperties["NugIcon"]=element.Value;
                    break;
                case "requireLicenseAcceptance":
                    properties.AfterProperties["NugRequireLicenseAcceptance"]=element.Value;
                    break;
                case "description":
                    properties.AfterProperties["NugDescription"]=element.Value;
                    break;
                case "summary":
                    properties.AfterProperties["NugSummary"] = element.Value;
                    break;
                case "releaseNotes":
                    properties.AfterProperties["NugReleaseNotes"]=element.Value;
                    break;
                case "copyright":
                    properties.AfterProperties["NugCopyright"] = element.Value;
                    break;
                case "language":
                    properties.AfterProperties["NugLang"] = element.Value;
                    break;
                case "title":
                    properties.AfterProperties["NugTitle"] = element.Value;
                    break;
                case "tags":
                    //manifestMetadata.Tags = value;
                    break;
                case "dependencies":
                    //manifestMetadata.Dependencies = ReadDependencies(element);
                    break;
                case "frameworkAssemblies":
                    //manifestMetadata.FrameworkAssemblies = ReadFrameworkAssemblies(element);
                    break;
                case "references":
                    //manifestMetadata.References = ReadReferences(element);
                    break;
            }
        }

        #region helper methods
        public DateTime? GetLastModified(string path)
        {
            if (Directory.Exists(path))
            {
                return Directory.GetLastWriteTimeUtc(path);
            }
            return File.GetLastAccessTimeUtc(path);
        }

        private static string GetSchemaNamespace(XDocument document)
        {
            string schemaNamespace = ManifestSchemaUtility.SchemaVersionV1;
            var rootNameSpace = document.Root.Name.Namespace;
            if (rootNameSpace != null && !String.IsNullOrEmpty(rootNameSpace.NamespaceName))
            {
                schemaNamespace = rootNameSpace.NamespaceName;
            }
            return schemaNamespace;
        }

        private static bool ValidateManifestSchema(XDocument document, string schemaNamespace, string siteUrl)
        {
            bool isValid = true;
            if (!CheckSchemaVersion(document))
            {
                return false;
            }

            // Create the schema set
            var schemaSet = new XmlSchemaSet();
            using (TextReader reader = ManifestSchemaUtility.GetSchemaReader(schemaNamespace, siteUrl))
            {
                schemaSet.Add(schemaNamespace, XmlReader.Create(reader));
            }

            // Validate the document
            document.Validate(schemaSet, (sender, e) =>
            {
                if (e.Severity == XmlSeverityType.Error)
                {
                    // Throw an exception if there is a validation error
                    //throw new InvalidOperationException(e.Message);
                    isValid = false;
                }
            });
            return isValid;
        }

        private static bool CheckSchemaVersion(XDocument document)
        {
            // Get the metadata node and look for the schemaVersion attribute
            XElement metadata = GetMetadataElement(document);

            if (metadata != null)
            {
                // Yank this attribute since we don't want to have to put it in our xsd
                XAttribute schemaVersionAttribute = metadata.Attribute(SchemaVersionAttributeName);

                if (schemaVersionAttribute != null)
                {
                    schemaVersionAttribute.Remove();
                }

                // Get the package id from the metadata node
                string packageId = GetPackageId(metadata);

                // If the schema of the document doesn't match any of our known schemas
                if (!ManifestSchemaUtility.IsKnownSchema(document.Root.Name.Namespace.NamespaceName))
                {
                    return false;
                }
            }
            return true;
        }

        private static XElement GetMetadataElement(XDocument document)
        {
            // Get the metadata element this way so that we don't have to worry about the schema version
            XName metadataName = XName.Get("metadata", document.Root.Name.Namespace.NamespaceName);

            return document.Root.Element(metadataName);
        }

        private static string GetPackageId(XElement metadataElement)
        {
            XName idName = XName.Get("id", metadataElement.Document.Root.Name.NamespaceName);
            XElement element = metadataElement.Element(idName);

            if (element != null)
            {
                return element.Value;
            }

            return null;
        }
        #endregion
    }
}
